﻿NoOpenElementException = function() {
    /// <summary>
    /// An exception class, thrown when a write operation is requested
    /// on a DOMWriter (e.g. to add an attribute) , and no element is 
    /// currently open
    /// </summary>
};

NoOpenElementException.prototype = {
    message: 'No element is currently open'
};


JSClientPageNotFoundException = function(pageName) {
    /// <summary>
    ///  An exception class, thrown when a compiled page is 
    ///  requested, and no page is bound to the specified name
    /// </summary>
    /// <param name="pageName">The name of the page that was requested</param>
    this.pageName = pageName;
    this.message = this.MessageTemplate.replace(
        '{0}',
        pageName
    );
};

JSClientPageNotFoundException.prototype = {
    /// <summary>The template used to format the error message</summary>
    MessageTemplate: "JavaScript Client Page '{0}' has not been compiled",

    /// <summary>The name of the page that was requested</summary>
    pageName: '',

    /// <summary>The error message</summary>
    message: ''
};


DOMWriter = function() {
    /// <summary>
    ///   Represents a writer that provides a fast, non-cached, forward-only means of generating
    ///   Document Object  Model (DOM) elements using the browser's HTML DOM. The idea is based
    ///   on .NET's XmlWriter class, which is also present in many other XML processing libraries.
    /// </summary>
    this.reset();
};

DOMWriter.Handlers = {
    Attribute: {
        'class': {
            setValue: function(element, value) {
                /// <summary>
                /// Sets the value of the "class" attribute. Because "class"
                /// is a reserved keyword in JavaScript, its DOM name was changed
                /// to "className"
                /// </summary>
                /// <param type="Element" name="element">
                ///   the element instance on which the attribute will be set
                /// </param>
                /// <param type="string" name="data">
                ///   The value of the attribute
                /// </param>
                element.className = value;
            }
        },
        'style': {
            setValue: function(element, value) {
                /// <summary>
                /// The "style" property of HTML elements is an object, not a
                /// string.
                /// </summary>
                /// <param type="Element" name="element">
                ///   the element instance on which the attribute will be set
                /// </param>
                /// <param type="string" name="data">
                ///   The value of the attribute
                /// </param>

                element.style.cssText = value;
            }
        }
    },
    Element: {
        tr: {
            start: function(parent) {
                var row = document.createElement('tr');
                if (parent.tagName.toLowerCase() == 'table') {
                    var tBodies = parent.getElementsByTagName('tbody');
                    if (tBodies.length == 0) {
                        var tbody = document.createElement('tbody');
                        tbody.appendChild(row);
                        parent.appendChild(tbody);
                    } else {
                        tBodies[0].appendChild(row);
                    }
                } else {
                    parent.appendChild(row);
                }
                return row;
            },
            end: function(parent, element) { }
        }
    }
}

DOMWriter.prototype = {
    /// <summary>
    ///     The top-level container
    /// </summary>
    container: null,

    /// <summary>
    /// A stack representing the current position in the DOM
    /// </summary>
    stack: [],

    reset: function() {
        /// <summary>
        /// Forgets about any previous output
        /// </summary>
        this.container = document.createElement('div');
        this.stack = [];
    },

    startElement: function(name) {
        /// <summary>
        /// Creates a new DOM element and pushes it onto the stack, to
        /// allow the creation of child elements
        /// </summary>
        /// <param name="name">the name of the element to create</param>

        var elementHandlers = DOMWriter.Handlers.Element;
        var element;

        if (elementHandlers[name] && elementHandlers[name].start) {
            element = elementHandlers[name].start.call(this, this.getParent());
        } else {
            element = document.createElement(name);
        }
        this.stack.push(element);
    },

    endElement: function() {
        /// <summary>
        /// Closes the element that is currently open and adds it to
        /// the its intended parent; namely, the next topmost element
        /// on the stack
        /// </summary>   
        if (this.stack.length == 0) {
            throw new NoOpenElementException();
        }
        var elementHandlers = DOMWriter.Handlers.Element;

        var element = this.stack.pop();
        var parent = this.getParent();
        var name = element.tagName.toLowerCase();
        if (elementHandlers[name] && elementHandlers[name].end) {
            element = elementHandlers[name].end.call(this, parent, element);
        } else {
            parent.appendChild(element);
        }

    },

    getParent: function() {
        /// <summary>
        /// Returns the topmost element of the stack of the container
        /// if the stack is empty
        /// </summary>
        return (this.stack.length == 0) ?
            this.container :
            this.stack[this.stack.length - 1];
    },


    writeAttribute: function(name, value) {
        /// <summary>
        /// Adds an attribute to the current element
        /// <param name="name" type="string">the name of the attribute (e.g. "id", "name", "rel", etc.)</param>
        /// <param name="value" type="string">the value to set</param>
        /// </summary>
        if (this.stack.length == 0) {
            throw new NoOpenElementException();
        }
        var element = this.stack.pop();

        if (DOMWriter.Handlers.Attribute[name]) {
            DOMWriter.Handlers.Attribute[name].setValue(element, value);
        } else {
            element[name] = value;
        }

        this.stack.push(element);
    },

    writeText: function(text) {
        /// <summary>
        /// Writes literal text to the current element by appending a DOM node to it.
        /// Warning: This method cannot be used to write HTML.
        /// </summary>
        /// <param name="text" type="string">the text to write</param>
        var parent = this.getParent();
        parent.appendChild(document.createTextNode(text));
    }
};

JSClientPage = function(compiledPage, target) {
    /// <summary>
    /// Represents a compiled JavaScript Client Page, which can be used
    /// to dynamically display various UI elements within a loaded XHTML
    /// page.
    /// </summary>
    /// <remarks>
    ///    Once a page has been compiled, it can be reused multiple times
    ///    to display different output using the same template.
    /// </remarks>  
    this.writer = new DOMWriter();
    this.write = compiledPage;
    this.target = target || document.documentElement;

};
JSClientPage.prototype = {
    /// <summary>
    /// The DOM Writer that will be used to write out
    /// the document information
    /// </summary>
    writer: null,

    /// <summary>
    /// The default target to which the page will be rendered if 
    /// no alternative target is specified as an argument
    /// </summary>
    target: null,

    /// <summary>
    /// The parameters that will be passed to the pages, accessible
    /// via the 'input' object
    /// </summary>
    parameters: {},

    assemblyAliases: {
        'se': ['startElement'],
        'ee': ['endElement'],
        'a': ['attribute'],
        't': ['text', 'writeText']
    },

    write: function(writer) {
        /// <summary>
        /// The function representing the compiled page. The default implementation
        /// does nothing.
        /// </summary>
        /// <param type="DOMWriter" name="writer">the DOM writer to which the output will be written</param>    
    },


    resolveTarget: function(target) {
        /// <summary>
        /// Resolves the value of the 'target' argument, which may be the ID of an element,
        /// an element instance or null to indicate the default target, to a DOM element. 
        /// - A DOM element
        /// </summary>
        /// <param name="target">
        /// An DOM element instance, the ID of an element or null for the
        /// default target
        /// </param>
        /// <return type="HTMLElement">A DOM element instance</return>
        var resolvedTarget;
        if (target === null) {
            resolvedTarget = this.target;
        } else if (typeof (target) == 'string') {
            resolvedTarget = document.getElementById(target);
        } else if (target.appendChild) {
            resolvedTarget = target;
        } else {
            target = null;
        }

        return resolvedTarget;
    },

    setParameter: function(name, value) {
        /// <summary>
        /// Sets a parameter to be used in the page. Once set, it will
        /// be accessible via the 'input' object
        /// </summary>
        /// <param name="name" type="string">
        /// The name of the parameter to be set. For instance,
        /// if it the name is 'item', it will be accessible as 'input.item'
        /// </param>
        /// <param name="value" type="object">
        /// The value of to which input[name] will evaluate on the page
        /// </param>

        this.parameters[name] = value;
    },

    getParameter: function(name, value) {
        /// <summary>
        /// Returns the value of a parameter previously set using getParameter()
        /// </summary>
        /// <param name="name" type="string"> the name of the parameter</param>
        /// <return type="object">the value or null if the parameter has not been defined</return>
        return this.parameters[name] || null;
    },

    clearParameters: function() {
        /// <summary>
        /// Removes all parameters previously set by setParameter();
        /// </summary>
        this.parameters = {};
    },

    getDOMAssembly: function() {
        /// <summary>
        /// Creates an object for the DOM "assembly" language,
        /// which contains a shorthand notation for common operations
        /// </summary>

        var writer = this.writer;
        var assembly =
        {
            se: function startElement(name) { writer.startElement(name); },
            ee: function endElement(name) { writer.endElement(); },
            t: function text(text) { writer.writeText(text) },
            a: function attribute(name, value) { writer.writeAttribute(name, value); },
            fe: function forEach(collection, itemName, lambda) {
                JSClientPage.Control.forEach(writer, collection, itemName, lambda);
            }
        };

        for (var key in this.assemblyAliases) {
            var aliasList = this.assemblyAliases[key];
            for (var i = 0; i < aliasList.length; i++) {
                assembly[aliasList[i]] = assembly[key];
            }
        }

        return assembly;
    },

    render: function(target) {
        /// <summary>
        /// Renders this JavaScript Client Page and appends the resulting output to
        /// the specified target
        /// </summary>
        /// <param name="target">
        /// The element within the current's document Document Object Model (DOM) to which
        /// the output will be appended. This method accepts both element instances and element
        /// IDs as long as they can be resolved properly. A null value indicates that the default
        /// target will be used
        /// </param>
        /// <remarks>
        /// If the target already contains child elements, they will NOT be removed. This is by
        /// design, since a common task with JavaScript Client Pages involves dynamically adding
        /// similar elements to the same parent in response to user commands. To dynamically refresh
        /// an element, use the refresh() method.
        /// </remarks>
        this.writer.reset();

        this.write(
            this.writer,
            this.parameters,
            this.getDOMAssembly()
        );

        var resolvedTarget = this.resolveTarget(target);
        resolvedTarget.appendChild(this.writer.container);
    },

    refresh: function(target, handleException) {
        /// <summary>
        /// Deletes all child elements from the target and then renders this
        /// JavaScript Client Page in it.
        /// </summary>
        /// <param name="target">
        /// The element within the current's document Document Object Model (DOM) to which
        /// the output will be appended. This method accepts both element instances and element
        /// IDs as long as they can be resolved properly. A null value indicates that the default
        /// target will be used
        /// </param>

        var resolvedTarget = this.resolveTarget(target);
        JSClientPage.clearTarget(resolvedTarget);
        JSClientPage.clearTarget(resolvedTarget); // Firefox needs this
        var page = this;
        page.render(resolvedTarget);
    }
};

JSClientPage.clearTarget = function(target) {

    var element = target.firstChild;
    while (element != null) {
        target.removeChild(element);
        element = element.nextSibling;
    }
}

/// <summary>
/// Compiled JavaScript Client Pages available in the current 
/// context.
/// </summary>
JSClientPage.pages =
{
    'empty': new JSClientPage()
};

JSClientPage.compile = function(pageName, writeFunction) {
    /// <summary>
    /// Statically registers a compiled page and associates it with a name.
    /// This function will be invoked by the compiler itself, and in most scenarios,
    /// there is no need for user code to call this function.
    /// </summary>
    this.pages[pageName] = new JSClientPage(writeFunction);
}

JSClientPage.getPage = function(pageName, parameters) {
    /// <summary>
    /// Returns the compiled JavaScript Client Page bound to the specified
    /// page name and sets the parameters if they have been provided
    /// </summary>
    /// <param name="pageName" type="string">the name of the page to look up</param>
    /// <param name="parameters" type="object"> the parameters that will be passed to the page</param>
    /// <return type="JSClientPage">a compiled JavaScript Client Page</return>

    var page = JSClientPage.pages[pageName];
    if (!page) {
        throw new JSClientPageNotFoundException(pageName);
    }

    if (parameters) {
        page.parameters = parameters;
    } else {
        page.clearParameters();
    }

    return page;
}

JSClientPage.render = function(pageName, target, parameters) {
    /// <summary>
    /// Renders a previously compiled JavaScript Client Page and appends the resulting output to
    /// the specified target
    /// </summary>
    /// <param name="pageName" type="string">
    /// The name of a compiled JavaScript client page
    /// </param>
    /// <param name="target">
    /// The element within the current's document Document Object Model (DOM) to which
    /// the output will be appended. This method accepts both element instances and element
    /// IDs as long as they can be resolved properly. A null value indicates that the default
    /// target will be used
    /// </param>
    /// <remarks>
    /// If the target already contains child elements, they will NOT be removed. This is by
    /// design, since a common task with JavaScript Client Pages involves dynamically adding
    /// similar elements to the same parent in response to user commands. To dynamically refresh
    /// an element, use the refresh() method.
    /// </remarks>

    try {
        var page = this.getPage(pageName);
        page.parameters = parameters;
        page.render(target);
    } catch (exception) {
        JSClientPage.handleException(pageName, exception);
    }
}

JSClientPage.handleException = function(pageName, exception) {
    if (pageName == "JSClientPageError") {
        throw exception;
    } else {
        JSClientPageError.handle(exception);
    }
}

JSClientPage.refresh = function(pageName, target, parameters) {
    /// <summary>
    /// Deletes all child elements from the target and then renders a previously compiled
    /// JavaScript Client Page in it.
    /// </summary>
    /// <param name="pageName" type="string">
    /// The name of a compiled JavaScript client page
    /// </param>
    /// <param name="target">
    /// The element within the current's document Document Object Model (DOM) to which
    /// the output will be appended. This method accepts both element instances and element
    /// IDs as long as they can be resolved properly. A null value indicates that the default
    /// target will be used
    /// </param>
//    try {
        var page = this.getPage(pageName);
        page.parameters = parameters;
        page.refresh(target);
//    } catch (exception) {
  //      JSClientPage.handleException(pageName, exception);
    //}
}



/// <summary>
/// Implementations of custom controls available in the 
/// JSElements schema
/// </summary>
JSClientPage.Control =
{
    Iterators: {
        Array: function(collection, lambda, itemName) {
            context = {};
            context[itemName] = null;
            for (var i = 0; i < collection.length; i++) {
                context[itemName] = collection[i];
                lambda(context);
            }
        },
        Object: function(lambda, itemName) {
            context = {};
            context[itemName] = null;
            for (var key in collection) {
                context[itemName] = collection[key];
                lambda(context);
            }
        }
    },
    forEach: function(writer, collection, itemName, lambda) {
        /// <summary>
        /// A lambda-based implementation of <js:forEach ...>
        /// that iterates over an array or a JavaScript collection
        /// </summary>
        /// <param name="writer" type="DOMWriter">an DOM Writer implementation</param>
        /// <param name="collection" type="object">an array or JSON-style object</param>
        /// <param name="itemName" type="string">
        /// the name of the variable to which individual elements will be assigned one 
        /// at a time.
        /// </param>
        /// <param name="lambda">
        /// the function that will be invoked on every iteration.
        /// </param>

        if (collection instanceof Array) {
            this.Iterators.Array(collection, lambda, itemName);
        } else {
            this.Iterators.Object(collection, lambda, itemName);
        }
    }
}


JSStackTrace = function(error) {
    /// <summary>
    /// Represents a stack trace that can be displayed on
    /// the error page.
    /// </summary>     
    /// <remarks>
    /// In JavaScript, there is no standard way to obtain a
    /// stack trace. Several common stack trace implementations
    /// will be provided for popular browsers. However, if a stack
    /// trace cannot be obtained, the error message will still be 
    /// displayed.
    /// </remarks> 
    if (error && this.isSupported()) {
        this.stackFrames = this.loadError(error);
    }
}

JSStackTrace.prototype =
{
    stackFrames: null,

    isSupported: function() {
        /// <summary>
        /// Determines if a stack trace can be provided
        /// on the current platform using this implementation
        /// </summary>
        return true;
    },

    getStackFrames: function() {
        /// <summary>
        /// <para>
        /// Returns an array of stack frames, each of which is formatted
        /// as follows:
        /// </para>
        /// <para>
        /// { line: [line number], method : [method name], file : [script source file] }
        /// </para>
        /// <return type="Array">an array of stack frames</return>
        return this.stackFrames;
    },

    loadError: function() {
        /// <summary>
        /// Extracts stack trace information from an error
        /// </summary>

        return [];
    }
}



JSStackTrace.extract = function(error) {
    /// <summary>
    /// Locates a platform-specific JSStackTrace implementation 
    /// for the current platform and uses it to extract the stack
    /// trace from the specified error object
    /// </summary>
    /// <param type="Error">an Error object, thrown as an exception</param>
    /// <return type="JSStackTrace">a stack trace instance</return>

    var stackTrace =
        JSStackTrace.Type.getValues()
        .select(
            function(type) {
                return new type(error);
            }
        ).findFirst(
            function(handler) {
                return handler.isSupported();
            }
        );

    if (!stackTrace) {
        stackTrace = new JSStackTrace(error);
    }

    return stackTrace;
}

JSStackTrace.Type = {};

JSStackTrace.Type.Mozilla = function(error) {
    /// <summary>
    /// Provides stack trace information for Mozilla derivatives
    /// </summary>
    JSStackTrace.call(this, error);
}

JSStackTrace.Type.Mozilla.prototype = new JSStackTrace();

JSStackTrace.Type.Mozilla.prototype.loadError = function(error) {
    /// <summary>
    /// Parses a Mozilla-style error
    /// </summary>
    /// <param type="Error">the error to parse</param>

    var frameLines =
        error.stack.split("\n").slice(1);
    var stackFrames = [];
    var regex = /^(.+)@(.+):(.+)/;
    if (frameLines) {
        stackFrames = frameLines.select(
            function(frameLine) {
                var frame = null;
                var matches = frameLine.toString().match(regex);
                if (matches) {
                    with (JSClientPageError.ErrorMessage) {
                        frame =
                        {
                            method: matches[1],
                            file: matches[2],
                            line: matches[3]
                        };
                    }
                }
                return frame;
            }
        ).findAll(
            function(frame) {
                return frame != null;
            }
        );
    }
    return stackFrames;
}

JSStackTrace.Type.Mozilla.prototype.isSupported = function() {
    var error = new Error();
    return error.stack && typeof (error.stack) == 'string';
}

Array.nonEnumerable = {
    'findFirst': true,
    'findAll': true,
    'select': true
}

Array.prototype.findFirst = function(predicate) {
    var result = undefined;
    for (var i = 0, length = this.length; i < length; i++) {
        var item = this[i];
        if (predicate(item)) {
            result = item;
            break;
        }
    }
    return result;
}

Array.prototype.peek = function() {
    return this[this.length - 1];
}

Array.prototype.findAll = function(predicate) {
    var result = [];
    for (var i = 0, length = this.length; i < length; i++) {
        var item = this[i];
        if (predicate(item)) {
            result.push(item);
        }
    }
    return result;
}

Array.prototype.select = function(projection) {

    var result = new Array(this.length);
    for (var i = 0; i < this.length; i++) {
        result[i] = projection(this[i]);
    }
    return result;
}


Object.prototype.getValues = function() {
    var values = [];
    for (var name in this) {
        if (name != 'getValues') {
            values.push(this[name]);
        }
    }
    return values;
}

Object._propertyIsEnumerable = Object.prototype.propertyIsEnumerable;

Object.globalNonEnumerable = {
    'getValues': true
};

Object.prototype.propertyIsEnumerable = function(propertyName) {
    var enumerable;
    if (
        propertyName == 'nonEnumerable'
            || Object.globalNonEnumerable[propertyName]
            || this.constructor.nonEnumerable && this.constructor.nonEnumerable[propertyName]
            || this.nonEnumerable && this.nonEnumerable[propertyName]
        ) {
        enumerable = false;
    } else {
        enumerable = Object._propertyIsEnumerable.call(this, propertyName);
    }
    return enumerable;
}

JSClientPageError = function(error) {
    /// <summary>
    /// Represents an error that occurred in a JavaScript client 
    /// page ot in the DiXon compiler.
    /// </summary>
    /// <param type="error">
    /// A JSON-style object containing the error
    /// message, the stack trace and the source identifier
    /// </param>

    if (error.message) {
        this.message = error.message;
    }
    if (error.stackTrace) {
        this.stackTrace = error.stackTrace;
    }
    if (error.source) {
        this.source = error.source;
    }
}

JSClientPage.windowLoaded = false;

JSClientPageError.prototype = {
    message: 'Unknown Error',
    stackTrace: null,
    source: "script",


    getTarget: function() {
        var target = null;
        var bodies = document.getElementsByTagName("BODY");
        if (bodies.length == 0) {
            target = document.documentElement;
        } else {
            target = bodies[0];
        }
        return target;
    },

    display: function() {
        var error = this;
        var parameters =
        {
            message: this.message,
            stackTrace: this.stackTrace,
            source: this.source
        };

        var render = function() {
            JSClientPage.refresh(
                'JSClientPageError',
                error.getTarget(),
                parameters
            );
        }
        if (JSClientPage.windowLoaded) {
            alert('already loaded');
            render();
        } else {
            var loadEventHandler = window.onload;
            window.onload = function() {
                if (loadEventHandler) {
                    loadEventHandler();
                }
                render();
            }
        }

    }

};

JSClientPageError.display = function(errorContent) {
    /// <summary>
    /// Displays an error message
    /// </summary>
    /// <param type="object">
    ///     the content of the error, formatted as
    /// {
    ///    message : [message text],
    ///    source : ['Compiler' or 'ClientPage'],
    ///    stackTrace : [{file: [file name], line: [source line], method: [method name]}]
    /// }
    /// </param>
    var error = new JSClientPageError(errorContent);
    error.display();
}

JSClientPageError.ErrorMessage = {
    ///<summary>Standard hard-coded error messages</summary>
    UnknownError: 'Unknown error',
    UnknownMethod: 'Unknown method',
    UnknownLine: 'Unknown line',
    UnknownFile: 'Unknown file'
}

JSClientPageError.displayOnLoad = function(errorContent) {
    /// <summary>
    /// Causes an error message to be displayed as soon as the
    /// page loads. This methid is useful for compiler-generated
    /// error messages, as well as any errors that occur while
    /// a JSCP-related script is loading (e.g. a platform incompatibility).
    /// </summary>
    window.onload = function() {
        var error = new JSClientPageError(errorContent);
        error.display();
    }
}




JSClientPageError.handle = function(scriptException) {
    /// <summary>
    /// Converts an error created by the JavaScript engine running
    /// user-space code to a JSClientPage error and displays it
    /// </summary>
    var error = JSClientPageError.ErrorMessage;
    var errorContent =
    {
        message:
            scriptException.message || error.UnknownError,
        stackTrace:
            JSStackTrace
                .extract(scriptException)
                .getStackFrames()
    };

    JSClientPageError.display(errorContent);
}

var loadEventHandler = window.onload;
window.onload = function() {
    JSClientPage.windowLoaded = true;
    if (loadEventHandler) {
        loadEventHandler();
    }
}